const reqHelper = require("./util/req.helper");
const lodash = require("lodash");
const UNIFIED_RETURN = {
  fieldCount: 0,
  affectedRows: 0,
  insertId: "",
  info: "",
  serverStatus: 6000,
  warningStatus: 0,
  changedRows: 0
};

//define class
class xctrl {
  constructor(app, mysql) {
    this.app = app;
    this.mysql = mysql;
  }

  getNotExistColumns(req) {
    let isSurroundedByBackticks = (str) => {
      const regex = /^`.*`$/;
      return regex.test(str);
    };
    let self = this;

    let tableName = reqHelper.getTableName(req).toLowerCase();
    let getCurrentTable = () => {
      const foundKey = Object.keys(self.mysql.metaDb.tables).find(
        (k) => k.toLowerCase() === tableName
      );
      if (foundKey) return self.mysql.metaDb.tables[foundKey];
      return undefined;
    };

    if (["mysql", "tidb"].includes(req.dialect.toLowerCase())) {
      let tableColumnDefs = getCurrentTable().columns.map((o) => {
        if (isSurroundedByBackticks(o.column_name)) {
          return o.column_name;
        }
        return "`" + o.column_name.replace(/`/g, "") + "`";
      });

      let notExistColumns = lodash.difference(
        Object.keys(req.body).map((o) => {
          if (isSurroundedByBackticks(o)) {
            return o;
          }
          return "`" + o.replace(/`/g, "") + "`";
        }),
        tableColumnDefs
      );

      return notExistColumns;
    }

    if (["dameng"].includes(req.dialect.toLowerCase())) {
      let tableColumnDefs = getCurrentTable().columns.map((o) => {
        return o.column_name;
      });

      let notExistColumns = lodash.difference(
        Object.keys(req.body).map((o) => {
          return o.toUpperCase();
        }),
        tableColumnDefs
      );

      return notExistColumns;
    }
  }

  async create(req, res) {
    let query = "INSERT INTO ?? (??) VALUES (?)";
    let params = [];
    for (let col of this.getNotExistColumns(req)) {
      delete req.body[col];
      delete req.body[col.slice(1, -1)];
    }

    let columns = Object.keys(req.body);
    let values = Object.values(req.body);
    if (!values || !Array.isArray(values)) {
      return res.status(200).json(Object.assign(UNIFIED_RETURN, { affectedRows: 0 }));
    }
    values = values.map((o) => {
      if (typeof o === 'object') {
        return JSON.stringify(o).replace(/\\"/g, '"').replace(/\\\\/g, '\\');
      }
      return o;
    });

    params.push(reqHelper.getTableName(req));
    params.push(columns);
    params.push(values);
    let results = await this.mysql.exec(query, params);

    if (!Array.isArray(results)) {
      res.status(200).json(results);
    } else {
      let unifiedReturn = Object.assign(UNIFIED_RETURN, { affectedRows: 1 });
      res.status(200).json(unifiedReturn);
    }
  }

  async list(req, res) {
    let { _refby, _include } = req.query;
    let queryParamsObj = {};
    queryParamsObj.query = "";
    queryParamsObj.params = [];

    this.mysql.prepareListQuery(req, res, queryParamsObj, 0);

    let results = await this.mysql.exec(
      queryParamsObj.query,
      queryParamsObj.params
    );

    if (_refby && results.length > 0) {
      let refTables = _refby.split(";");
      for (let refTable of refTables) {
        await this.mysql.refBy(results, refTable);
      }
    }

    if (_include && results.length > 0) {
      let includeTables = _include.split(";");
      for (let includeTable of includeTables) {
        await this.mysql.include(results, includeTable);
      }
    }

    let total = await this.total(req.query._where, reqHelper.getTableName(req));
    let page = parseInt(req.query._p || "0");
    let size = parseInt(req.query._size || "10");
    res.status(200).json({
      results,
      total: total[0].no_of_rows,
      page,
      size,
    });
  }

  async nestedList(req, res) {
    let queryParamsObj = {};
    queryParamsObj.query = "";
    queryParamsObj.params = [];

    this.mysql.prepareListQuery(req, res, queryParamsObj, 1);

    let results = await this.mysql.exec(
      queryParamsObj.query,
      queryParamsObj.params
    );
    res.status(200).json(results);
  }

  async findOne(req, res) {
    let queryParamsObj = {};
    queryParamsObj.query = "";
    queryParamsObj.params = [];

    this.mysql.prepareListQuery(req, res, queryParamsObj, 2);

    let results = await this.mysql.exec(
      queryParamsObj.query,
      queryParamsObj.params
    );
    res.status(200).json(results);
  }

  async read(req, res) {
    let query = "select * from ?? where ";
    let params = [];

    params.push(reqHelper.getTableName(req));

    let clause = this.mysql.getPrimaryKeyWhereClause(
      reqHelper.getTableName(req),
      req.params.id.split("___")
    );

    if (!clause) {
      return res.status(400).send({
        error:
          "Table is made of composite primary keys - all keys were not in input",
      });
    }

    query += clause;
    query += " LIMIT 1";

    let results = await this.mysql.exec(query, params);
    res.status(200).json(results);
  }

  async exists(req, res) {
    let query = "select * from ?? where ";
    let params = [];

    params.push(reqHelper.getTableName(req));

    let clause = this.mysql.getPrimaryKeyWhereClause(
      reqHelper.getTableName(req),
      req.params.id.split("___")
    );

    if (!clause) {
      return res.status(400).send({
        error:
          "Table is made of composite primary keys - all keys were not in input",
      });
    }

    query += clause;
    query += " LIMIT 1";

    let results = await this.mysql.exec(query, params);
    res.status(200).json(results);
  }

  async update(req, res) {
    let query = "REPLACE INTO ?? SET ?";
    let params = [];

    params.push(reqHelper.getTableName(req));
    params.push(req.body);

    let results = await this.mysql.exec(query, params);
    res.status(200).json(results);
  }

  async patch(req, res) {
    let query = "UPDATE ?? SET ";
    for (let col of this.getNotExistColumns(req)) {
      delete req.body[col];
      delete req.body[col.slice(1, -1)];
    }
    let keys = Object.keys(req.body);

    // SET clause
    let updateKeys = "";
    for (let i = 0; i < keys.length; ++i) {
      updateKeys += keys[i] + " = ? ";
      if (i !== keys.length - 1) updateKeys += ", ";
    }

    // where clause
    query += updateKeys + " where ";
    let clause = this.mysql.getPrimaryKeyWhereClause(
      reqHelper.getTableName(req),
      req.params.id.split("___")
    );

    if (!clause) {
      return res.status(400).send({
        error:
          "Table is made of composite primary keys - all keys were not in input",
      });
    }

    query += clause;

    // params
    let params = [];
    params.push(reqHelper.getTableName(req));
    params = params.concat(Object.values(req.body));

    let results = await this.mysql.exec(query, params);
    if (!Array.isArray(results)) {
      res.status(200).json(results);
    } else {
      let unifiedReturn = Object.assign(UNIFIED_RETURN, { affectedRows: 1 });
      res.status(200).json(unifiedReturn);
    }
  }

  async delete(req, res) {
    let query = "DELETE FROM ?? WHERE ";
    let params = [];

    params.push(reqHelper.getTableName(req));

    let clause = this.mysql.getPrimaryKeyWhereClause(
      reqHelper.getTableName(req),
      req.params.id.split("___")
    );

    if (!clause) {
      return res.status(400).send({
        error:
          "Table is made of composite primary keys - all keys were not in input",
      });
    }

    query += clause;

    let results = await this.mysql.exec(query, params);
    if (!Array.isArray(results)) {
      res.status(200).json(results);
    } else {
      let unifiedReturn = Object.assign(UNIFIED_RETURN, { affectedRows: params.length });
      res.status(200).json(unifiedReturn);
    }
  }

  async bulkInsert(req, res) {
    let queryParamsObj = {};
    queryParamsObj.query = "";
    queryParamsObj.params = [];
    this.mysql.prepareBulkInsert(
      reqHelper.getTableName(req),
      req.body,
      queryParamsObj
    );

    let results = await this.mysql.exec(
      queryParamsObj.query,
      queryParamsObj.params
    );
    if (!Array.isArray(results)) {
      res.status(200).json(results);
    } else {
      let unifiedReturn = Object.assign(UNIFIED_RETURN, { affectedRows: (req.body || []).length });
      res.status(200).json(unifiedReturn);
    }
  }

  async bulkDelete(req, res) {
    let query = "delete from ?? where ?? in ";
    let params = [];

    params.push(reqHelper.getTableName(req));
    params.push(this.mysql.getPrimaryKeyName(reqHelper.getTableName(req)));

    query += "(";

    if (req.query && req.query._ids) {
      let ids = req.query._ids.split(",");
      for (var i = 0; i < ids.length; ++i) {
        if (i) {
          query += ",";
        }
        query += "?";
        params.push(ids[i]);
      }
    }

    query += ")";

    let { _refcheck } = req.query;
    if (_refcheck) {
      let ands = [];
      let refTables = _refcheck.split(";");
      for (let refTable of refTables) {
        let refDefines = refTable.split(",");
        if (refDefines.length != 3) {
          continue;
        }
        let colName = refDefines[0];
        let tableName = refDefines[1];
        let refColName = refDefines[2];
        ands.push(
          `${colName} NOT IN (SELECT DISTINCT(${refColName}) FROM ${tableName} WHERE ${refColName} IS NOT NULL)`
        );
      }
      query += ` AND ${ands.join(" AND ")}`;
    }

    let results = await this.mysql.exec(query, params);
    if (!Array.isArray(results)) {
      res.status(200).json(results);
    } else {
      let unifiedReturn = Object.assign(UNIFIED_RETURN, { affectedRows: params.length - 2 });
      res.status(200).json(unifiedReturn);
    }
  }

  async bulkRead(req, res) {
    let queryParamsObj = {};
    queryParamsObj.query = "";
    queryParamsObj.params = [];

    this.mysql.prepareListQuery(req, res, queryParamsObj, 3);

    let results = await this.mysql.exec(
      queryParamsObj.query,
      queryParamsObj.params
    );
    res.status(200).json(results);
  }

  async total(where, tableName) {
    let queryParams = {};

    queryParams.query = "select count(1) as no_of_rows from ?? ";
    queryParams.params = [];
    queryParams.params.push(tableName);
    this.mysql.getWhereClause(where, tableName, queryParams, " where ");

    return await this.mysql.exec(queryParams.query, queryParams.params);
  }

  async count(req, res) {
    res
      .status(200)
      .json(await this.total(req.query._where, reqHelper.getTableName(req)));
  }

  async distinct(req, res) {
    let queryParamsObj = {};
    queryParamsObj.query = "";
    queryParamsObj.params = [];

    this.mysql.prepareListQuery(req, res, queryParamsObj, 4);

    let results = await this.mysql.exec(
      queryParamsObj.query,
      queryParamsObj.params
    );
    res.status(200).json(results);
  }

  async groupBy(req, res) {
    if (req.query && req.query._fields) {
      let queryParamsObj = {};
      queryParamsObj.query = "select ";
      queryParamsObj.params = [];

      /**************** add columns and group by columns ****************/
      this.mysql.getColumnsForSelectStmt(
        reqHelper.getTableName(req),
        req.query,
        queryParamsObj
      );

      queryParamsObj.query += ",count(*) as _count from ?? group by ";
      let tableName = reqHelper.getTableName(req);
      queryParamsObj.params.push(tableName);

      this.mysql.getColumnsForGroupBy(
        reqHelper.getTableName(req),
        req.query,
        queryParamsObj
      );

      if (!req.query._sort) {
        req.query._sort = {};
        req.query._sort = "-_count";
      }

      /**************** add having clause ****************/
      this.mysql.getHavingClause(
        req.query._having,
        reqHelper.getTableName(req),
        queryParamsObj,
        " having "
      );

      /**************** add orderby clause ****************/
      this.mysql.getOrderByClause(req.query, tableName, queryParamsObj);
      let results = await this.mysql.exec(
        queryParamsObj.query,
        queryParamsObj.params
      );

      res.status(200).json(results);
    } else {
      res.status(400).json({
        message:
          "Missing _fields query params eg: /api/tableName/groupby?_fields=column1",
      });
    }
  }

  async ugroupby(req, res) {
    if (req.query && req.query._fields) {
      let queryParamsObj = {};
      queryParamsObj.query = "";
      queryParamsObj.params = [];
      let uGrpByResults = {};

      /**************** add fields with count(*) *****************/
      let fields = req.query._fields.split(",");

      for (var i = 0; i < fields.length; ++i) {
        uGrpByResults[fields[i]] = [];

        if (i) {
          queryParamsObj.query += " UNION ";
        }
        queryParamsObj.query +=
          " SELECT IFNULL(CONCAT(?,?,??),?) as ugroupby, count(*) as _count from ?? GROUP BY ?? ";
        queryParamsObj.params.push(fields[i]);
        queryParamsObj.params.push("~");
        queryParamsObj.params.push(fields[i]);
        queryParamsObj.params.push(fields[i] + "~");
        queryParamsObj.params.push(reqHelper.getTableName(req));
        queryParamsObj.params.push(fields[i]);
      }

      let results = await this.mysql.exec(
        queryParamsObj.query,
        queryParamsObj.params
      );

      for (var i = 0; i < results.length; ++i) {
        let grpByColName = results[i]["ugroupby"].split("~")[0];
        let grpByColValue = results[i]["ugroupby"].split("~")[1];

        let obj = {};
        obj[grpByColValue] = results[i]["_count"];

        uGrpByResults[grpByColName].push(obj);
      }

      res.status(200).json(uGrpByResults);
    } else {
      res.status(400).json({
        message:
          "Missing _fields query params eg: /api/tableName/ugroupby?_fields=column1,column2",
      });
    }
  }

  async aggregate(req, res) {
    if (req.query && req.query._fields) {
      let tableName = reqHelper.getTableName(req);
      let query = "select ";
      let params = [];
      let fields = req.query._fields.split(",");

      for (var i = 0; i < fields.length; ++i) {
        if (i) {
          query = query + ",";
        }
        query =
          query +
          " min(??) as ?,max(??) as ?,avg(??) as ?,sum(??) as ?,stddev(??) as ?,variance(??) as ? ";
        params.push(fields[i]);
        params.push("min_of_" + fields[i]);
        params.push(fields[i]);
        params.push("max_of_" + fields[i]);
        params.push(fields[i]);
        params.push("avg_of_" + fields[i]);
        params.push(fields[i]);
        params.push("sum_of_" + fields[i]);
        params.push(fields[i]);
        params.push("stddev_of_" + fields[i]);
        params.push(fields[i]);
        params.push("variance_of_" + fields[i]);
      }

      query = query + " from ??";
      params.push(tableName);

      let results = await this.mysql.exec(query, params);

      res.status(200).json(results);
    } else {
      res.status(400).json({
        message:
          "Missing _fields in query params eg: /api/tableName/aggregate?_fields=numericColumn1",
      });
    }
  }

  async chart(req, res) {
    let query = "";
    let params = [];
    let obj = {};

    if (req.query) {
      let isRange = false;
      if (req.query.range) {
        isRange = true;
      }

      if (req.query && req.query.min && req.query.max && req.query.step) {

        obj = this.mysql.getChartQueryAndParamsFromMinMaxStep(
          reqHelper.getTableName(req),
          req.query._fields,
          parseInt(req.query.min),
          parseInt(req.query.max),
          parseInt(req.query.step),
          isRange
        );
      } else if (
        req.query &&
        req.query.steparray &&
        req.query.steparray.length > 1
      ) {
        obj = this.mysql.getChartQueryAndParamsFromStepArray(
          reqHelper.getTableName(req),
          req.query._fields,
          req.query.steparray.split(",").map(Number),
          isRange
        );
      } else if (
        req.query &&
        req.query.steppair &&
        req.query.steppair.length > 1
      ) {
        obj = this.mysql.getChartQueryAndParamsFromStepPair(
          reqHelper.getTableName(req),
          req.query._fields,
          req.query.steppair.split(",").map(Number),
          false
        );
      } else {
        query =
          "select min(??) as min,max(??) as max,stddev(??) as stddev,avg(??) as avg from ??";
        params = [];

        params.push(req.query._fields);
        params.push(req.query._fields);
        params.push(req.query._fields);
        params.push(req.query._fields);
        params.push(reqHelper.getTableName(req));

        let _this = this;

        let results = await _this.mysql.exec(query, params);

        obj = _this.mysql.getChartQueryAndParamsFromMinMaxStddev(
          reqHelper.getTableName(req),
          req.query._fields,
          results[0]["min"],
          results[0]["max"],
          results[0]["stddev"],
          isRange
        );
      }

      this.mysql.getWhereClause(
        req.query._where,
        reqHelper.getTableName(req),
        obj,
        " where "
      );

      let results = await this.mysql.exec(obj.query, obj.params);

      res.status(200).json(results);
    } else {
      res.status(400).json({
        message:
          "Missing _fields in query params eg: /api/tableName/chart?_fields=numericColumn1",
      });
    }
  }

  async autoChart(req, res) {
    let query = "describe ??";
    let params = [reqHelper.getTableName(req)];
    let obj = {};
    let results = [];

    let isRange = false;
    if (req.query.range) {
      isRange = true;
    }

    let describeResults = await this.mysql.exec(query, params);

    for (var i = 0; i < describeResults.length; ++i) {
      if (
        describeResults[i]["Key"] !== "PRI" &&
        this.mysql.isTypeOfColumnNumber(describeResults[i]["Type"])
      ) {
        query =
          "select min(??) as min,max(??) as max,stddev(??) as stddev,avg(??) as avg from ??";
        params = [];

        params.push(describeResults[i]["Field"]);
        params.push(describeResults[i]["Field"]);
        params.push(describeResults[i]["Field"]);
        params.push(describeResults[i]["Field"]);
        params.push(reqHelper.getTableName(req));

        let _this = this;

        let minMaxResults = await _this.mysql.exec(query, params);

        query = "";
        params = [];

        obj = _this.mysql.getChartQueryAndParamsFromMinMaxStddev(
          reqHelper.getTableName(req),
          describeResults[i]["Field"],
          minMaxResults[0]["min"],
          minMaxResults[0]["max"],
          minMaxResults[0]["stddev"],
          isRange
        );

        let r = await this.mysql.exec(obj.query, obj.params);

        let resultObj = {};
        resultObj["column"] = describeResults[i]["Field"];
        resultObj["chart"] = r;

        results.push(resultObj);
      }
    }

    res.status(200).json(results);
  }
}

//expose class
module.exports = xctrl;
